﻿using System;
using System.Collections.Generic;
using System.IO;
using System.ServiceProcess;
using DotNet.DevOps.Tasks.Service.Helpers;
using DotNet.DevOps.Tasks.Util;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace DotNet.DevOps.Tasks.Service
{
    public interface IInstallService
    {
        string Name { get; set; }
        string Machine { get; set; }
        string LogonAsUser { get; set; }
        string LogonAsPassword { get; set; }
        string ServiceExecutable { get; set; }
        string ServiceArguments { get; set; }
        string SourcePath { get; set; }
        string DestinationPath { get; set; }
        string Description { get; set; }
        bool StartWhenDone { get; set; }
        string Startup { get; set; }
        int Timeout { get; set; }
    }

    /// <summary>
    /// Can install a new or replace an existing windows service.
    /// The following actions are taking, in order.
    /// 
    /// * Stop
    /// * Uninstall
    /// * Copy (unzip if needed)
    /// * Install
    /// * Start 
    /// 
    /// on a local or remote machine.
    /// It is possible to define logon user, timeout and service description, among other things.
    /// 
    /// Requirements:
    /// * The user under which the task is executed must have the correct rights to install, uninstall, start, stop, copy on the local/remote machine.
    /// 
    /// Default behaviour:
    /// * Installs service to run under SYSTEM account.
    /// * Installs on this machine if no machine specified.
    /// * Installs with default description, service name.
    /// * No timeout.
    /// </summary>
    /// <example>
    /// import-module .\DotNet.DevOps.Tasks.dll
    /// import-module .\Ionic.Zip.Reduced.dll
    /// 
    /// $serviceProps = @{
    ///     Name = "TestService"
    ///     Machine = "TESTSERVER" 
    ///     ServiceExecutable = "NServiceBus.Host.Exe" 
    ///     ServiceArguments = "-service MSBuild.DevOps.Tasks.Tests.dll /serviceName:TestService" 
    ///     SourcePath = "MyService.zip" 
    ///     DestinationPath = "\\Server\MyService" 
    ///     Description = "This is my service" 
    ///     Startup = "demand"
    ///     Timeout = "30000"
    /// }
    /// $globalProps = @{
    ///     AppConfig = "MyService.dll.config"
    ///     Log4net = "log4net.config"
    ///     DataSource = "DEV"
    ///     RootLogger = "WARN"
    ///     NHBLogger = "WARN"
    /// }
    /// 
    /// $deploy = New-Object DotNet.DevOps.Tasks.Service.InstallService -Property $serviceProps
    ///  
    /// $deploy.SetAppConfig($globalProps["AppConfig"])
    /// $deploy.SetAppSetting("DataSource",$globalProps["DataSource"])
    /// $deploy.SetLog4NetConfig($globalProps["Log4net"])
    /// $deploy.SetRootLogger($globalProps["RootLogger"])
    /// $deploy.SetLogger("NHibernate",$globalProps["NHBLogger"])
    /// 
    /// $result = $deploy.Execute()
    /// </example>
    /// <example>
    /// <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    /// 
    ///  <UsingTask TaskName="InstallService" AssemblyFile="$(OutputDir)\DotNet.DevOps.Tasks.dll" />
    /// 
    ///  <Target Name="Build">
    /// 
    ///    <InstallService   Name="MyService" 
    ///                     Machine="TESTSERVER" 
    ///                     ServiceExecutable="NServiceBus.Host.Exe" 
    ///                     ServiceArguments="-service MSBuild.DevOps.Tasks.Tests.dll /serviceName:TestService" 
    ///                     SourcePath="MyService.zip" 
    ///                     DestinationPath="\\Server\MyService" 
    ///                     Description="This is my service" 
    ///                     Startup="demand"
    ///                     Timeout="30000" />
    /// 
    ///  </Target>
    /// 
    /// </Project>
    /// </example>
    /// <example>
    /// </example>
    public class InstallService : ITask, IInstallService
    {
        public IBuildEngine BuildEngine { get; set; }
        public ITaskHost HostObject { get; set; }

        /// <summary>
        /// Name of the service to deploy.
        /// </summary>
        [Required]
        public string Name { get; set; }


        private string _machine;
        /// <summary>
        /// Name of the machine on which to install the service. Default to this machine.
        /// </summary>
        [Standard]
        public string Machine
        {
            get
            {
                return string.IsNullOrWhiteSpace(_machine) == true ? Environment.MachineName : _machine;
            }

            set { _machine = value; }
        }

        /// <summary>
        /// Name of the user under which the service will execute. Defaults to SYSTEM.
        /// </summary>
        [Standard]
        public string LogonAsUser { get; set; }

        /// <summary>
        /// Password of the user under which the service will execute. Will be auto prefixed with machine name if you forget.
        /// </summary>
        [Standard]
        public string LogonAsPassword { get; set; }

        /// <summary>
        /// Name of the service executable.
        /// </summary>
        [Required]
        public string ServiceExecutable { get; set; }

        /// <summary>
        /// Arguments to the service executable.
        /// </summary>
        [Standard]
        public string ServiceArguments { get; set; }

        /// <summary>
        /// Path where the service binaries can be found. Zip files will be unpacked directly into destination.
        /// </summary>
        [Required]
        public string SourcePath { get; set; }

        /// <summary>
        /// Path where the service binaries must be placed.
        /// </summary>
        [Required]
        public string DestinationPath { get; set; }

        /// <summary>
        /// Description of service to display in service.msc.
        /// </summary>
        [Standard]
        public string Description { get; set; }

        /// <summary>
        /// Specifies wether the service should be started upon successfull completion of the install. Default true.
        /// </summary>
        [Standard]
        public bool StartWhenDone { get; set; }

        /// <summary>
        /// Specifies the service startup type. Valid values are: auto, demand, disabled. Default auto.
        /// </summary>
        [Standard]
        public string Startup { get; set; }

        /// <summary>
        /// Complete timeout, in milliseconds, for all the service command which might fire (Stop, Uninstall, Copy, Install, Start). Default infinte.
        /// </summary>
        [Standard]
        public int Timeout { get; set; }

        private string appConfigPath = "";
        private string log4netConfigPath = "";

        private ServiceControl controller;
        private ServiceManager manager;
        private FileManager fileManager;

        private List<ITask> subTasksToRun = new List<ITask>();

        private ILog Log { get; set; }

        public InstallService()
        {
            Init();
            Log = new MSBuildLogger(this);
        }

        public InstallService(ILog log)
        {
            Init();
            Log = log;
        }

        private void Init()
        {
            StartWhenDone = true;
            Startup = "auto";
            Timeout = -1;
        }

        private void InitializeHelpers()
        {
            controller = new ServiceControl(Log)
                            {
                                CommandTimeout = Timeout,
                                Machine = Machine,
                                Name = Name,
                            };

            manager = new ServiceManager(Log)
            {
                    CommandTimeout = Timeout,
                    Machine = Machine,
                    Name = Name,
                    ExecutablePath = Path.Combine(DestinationPath, ServiceExecutable),
                    ExecutableArguments = ServiceArguments,
                    Description = Description,
                    LogonUser = LogonAsUser,
                    LogonPassword = LogonAsPassword,
                    Startup = Startup
            };

            fileManager = new FileManager(Log, Name);
        }

        public bool Execute()
        {
            InitializeHelpers();

            try
            {
                Install();

                Update();

                Start();
            }
            catch (Exception ex)
            {
                Log.LogError(ex);

                Log.LogInfo(string.Format("{0} - Installation FAILURE.", Name));
                return false;
            }

            Log.LogInfo(string.Format("{0} - Installation SUCCESS.", Name));

            return true;
        }

        private void Install()
        {
            ValidateProperties();

            InvokeActionWithExceptionSupressing(controller.Stop);

            InvokeActionWithExceptionSupressing(manager.UnInstall);

            fileManager.Copy(SourcePath, DestinationPath);

            manager.Install();

            manager.SetDescription();
        }

        private void Start()
        {
            if (StartWhenDone == true)
            {
                controller.Start();
            }
        }

        private void Update()
        {
            if (subTasksToRun.Count > 0)
            {
                Log.LogInfo(string.Format("{0} - Executing subtasks:", Name));
                subTasksToRun.ForEach(ExecuteSubTask);
            }
        }

        private void ExecuteSubTask(ITask task)
        {
            task.BuildEngine = this.BuildEngine;

            if(task.Execute() == false)
            {
                throw new Exception(string.Format("Failed subtask: {0}.\r\nCheck previous errors.", task));
            }
        }

        private void ValidateProperties()
        {
            VerifyExecutable();
            VerifyMachine();
        }

        private void VerifyExecutable()
        {
            if(fileManager.CheckFileExists(SourcePath,ServiceExecutable) == false)
            {
                var msg = string.Format("The file {0} could not be found in {1}.",ServiceExecutable,SourcePath);
                throw new Exception(msg);
            }
        }

        private void VerifyMachine()
        {
            ServiceController.GetServices(Machine);
        }

        private void InvokeActionWithExceptionSupressing(Action action)
        {
            try
            {
                action.Invoke();
            }
            catch (Exception ex)
            {
                Log.LogWarning(string.Format("(Ignored) {0}", ex.Message));
            }
        }

        /// <summary>
        /// Specify which app.config file to use releative to the specified DestinationPath.
        /// </summary>
        /// <param name="appConfigFilenameAndPath">app.config filename and path, releative to the specified DestinationPath.</param>
        public void SetAppConfig(string appConfigFilenameAndPath)
        {
            appConfigPath = Path.Combine(DestinationPath, appConfigFilenameAndPath);
        }

        /// <summary>
        /// Specify appsetting key to update with value.
        /// </summary>
        /// <param name="key">App setting Key to find.</param>
        /// <param name="value">App setting Value to set.</param>
        public void SetAppSetting(string key, string value)
        {
            if (string.IsNullOrEmpty(appConfigPath))
            {
                throw new Exception("You have not specified a AppConfig file. Use InstallService().AppConfig(path).AppSetting(...).");
            }

            subTasksToRun.Add(new AppConfig.AppSetting(Log)
                               {
                                   Files = appConfigPath,
                                   Key = key,
                                   Value = value,
                               });
        }

        /// <summary>
        /// Specify which log4net.config file to use releative to the specified DestinationPath.
        /// </summary>
        /// <param name="log4netConfigFilenameAndPath">log4net.config filename and path, releative to the specified DestinationPath.</param>
        public void SetLog4NetConfig(string log4netConfigFilenameAndPath)
        {
            log4netConfigPath = Path.Combine(DestinationPath, log4netConfigFilenameAndPath);
        }

        /// <summary>
        /// Specify logger to update with level.
        /// </summary>
        /// <param name="loggerName">Name of logger.</param>
        /// <param name="level">Log level.</param>
        public void SetLogger(string loggerName, string level)
        {
            if (string.IsNullOrEmpty(log4netConfigPath))
            {
                throw new Exception("You have not specified a log4netConfigPath file. Use InstallService().Log4Net(path).Logger(...).");
            }

            subTasksToRun.Add(new Log4Net.Logger(Log)
            {
                Files = log4netConfigPath,
                Name = loggerName,
                Level = level,
            });
        }

        /// <summary>
        /// Specify root level.
        /// </summary>
        /// <param name="level">Log level.</param>
        public void SetRootLogger(string level)
        {
            if (string.IsNullOrEmpty(log4netConfigPath))
            {
                throw new Exception("You have not specified a log4netConfigPath file. Use InstallService().Log4Net(path).Root(...).");
            }

            subTasksToRun.Add(new Log4Net.Root(Log)
            {
                Files = log4netConfigPath,
                Level = level,
            });
        }
    }
}
